/*
 * Vimplugin
 *
 * Copyright (c) 2007 - 2011 by The Vimplugin Project.
 *
 * Released under the GNU General Public License
 * with ABSOLUTELY NO WARRANTY.
 *
 * See the file COPYING for more information.
 */
package org.vimplugin;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;

import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;

import java.util.HashSet;

import java.util.regex.Pattern;

import org.vimplugin.editors.VimEditor;

import org.vimplugin.listeners.BufferEnter;
import org.vimplugin.listeners.FeedKeys;
import org.vimplugin.listeners.FileClosed;
import org.vimplugin.listeners.FileModified;
import org.vimplugin.listeners.FileOpened;
import org.vimplugin.listeners.FileUnmodified;
import org.vimplugin.listeners.IVimListener;
import org.vimplugin.listeners.Logger;
import org.vimplugin.listeners.ServerDisconnect;
import org.vimplugin.listeners.ServerStarted;
import org.vimplugin.listeners.TextInsert;
import org.vimplugin.listeners.TextRemoved;

import org.vimplugin.preferences.PreferenceConstants;

/**
 * Manage the communication channel with Vim. This is the main interface to a
 * Vim instance. Important functions are: start/close tcp communication, and
 * sending of commands/functions. The protocol is explained in detail at vimdoc.
 * Events generated by Vim are consumed by
 * {@link org.vimplugin.listeners.IVimListener Listeners} (ObserverPattern).
 *
 * @see <a
 *      href="http://www.vim.org/htmldoc/netbeans.html#netbeans-protocol">Protocol
 *      specification</a>
 *
 */
public class VimConnection implements Runnable
{
  private static final org.eclim.logging.Logger logger =
    org.eclim.logging.Logger.getLogger(VimConnection.class);

  /* pattern to match events lines to differencient from function response while
     waiting on function response */
  private static final Pattern EVENT = Pattern.compile("^\\d+:\\w+=\\d+.*");

  /** is a vim instance running? */
  private boolean serverRunning = false;

  /** did the vim instance report "startupDone"? */
  private boolean startupDone = false;

  /** the set of VimListeners. Observer-Pattern. */
  final HashSet<IVimListener> listeners = new HashSet<IVimListener>();

  /** the id of the calling vim instance (as given in VimServer) */
  final int vimID;

  /** the channel we can get messages from */
  private BufferedReader in;

  /** the channel we can write messages to */
  private PrintWriter out;

  private final int port;

  private ServerSocket socket;

  /** the socket the vim instance runs on */
  private Socket vimSocket;

  private volatile boolean functionCalled = false;
  private Object functionMonitor = new Object();
  private String functionResult;

  /** creates a connection object (but does not start the connection ..). */
  public VimConnection(int instanceID) {
    port = VimPlugin.getDefault().getPreferenceStore().getInt(
        PreferenceConstants.P_PORT);
    vimID = instanceID;
  }

  /**
   * Establishes a TCP-Connection, adds
   * {@link org.vimplugin.listeners.IVimListener listeners} and creates VimEvents to be
   * consumed by the listeners.
   *
   * @see java.lang.Runnable#run()
   */
  public void run() {
    try {
      // start server
      logger.debug("Server starting on port " + (port + vimID));
      socket = new ServerSocket(port + vimID);
      logger.debug("Server started and listening");

      // accept client
      vimSocket = socket.accept();
      out = new PrintWriter(vimSocket.getOutputStream(), true);
      in = new BufferedReader(new InputStreamReader(vimSocket
          .getInputStream()));
      logger.debug("Connection established");

      // Add Listeners
      listeners.add(new Logger());
      listeners.add(new ServerStarted());
      listeners.add(new ServerDisconnect());
      listeners.add(new TextInsert());
      listeners.add(new TextRemoved());
      listeners.add(new FileOpened());
      listeners.add(new FileClosed());
      listeners.add(new FileModified());
      listeners.add(new FileUnmodified());
      listeners.add(new BufferEnter());
      listeners.add(new FeedKeys());

      // handle Events
      String line;

      try {
        while (!startupDone && (line = in.readLine()) != null) {
          //ignore "special messages" (see :help nb-special)
          if (!line.startsWith("AUTH")) {
            VimEvent ve = new VimEvent(line, this);
            for (IVimListener listener : listeners) {
              listener.handleEvent(ve);
            }
          }
        }
      } catch (VimException ve) {
        // TODO : better ErrorHandling (Connection Thread)
        logger.error("error:", ve);
      }

      try {
        while ((serverRunning && (line = in.readLine()) != null)) {
          if(functionCalled && !EVENT.matcher(line).matches()){
            synchronized(functionMonitor){
              functionResult = line;
              functionMonitor.notify();
            }
          }else{
            VimEvent ve = new VimEvent(line, this);
            for (IVimListener listener : listeners) {
              listener.handleEvent(ve);
            }
          }
        }
      } catch (SocketException se) {
        // the connection to vim was closed, so close the editor.
        VimPlugin plugin = VimPlugin.getDefault();
        VimServer server = plugin.getVimserver(getVimID());
        if (server != null){
          for (VimEditor editor : server.getEditors()){
            if (editor != null) {
              editor.forceDispose();
            }
          }
        }
        close();
      }
    } catch (Exception e) {
      logger.error("error:", e);
      throw new RuntimeException(e);
    }
  }

  /**
   * shuts down the TCP-Connection to the vim instance.
   *
   * @return always true.
   */
  public boolean close() throws IOException {
    if (vimSocket != null){
      vimSocket.close();
    }
    if (socket != null){
      socket.close();
    }
    serverRunning = false;
    return true;
  }

  /**
   * Sends a <i>command</i> (no replay) as specified by the netbeans-protocol
   * to the vim instance.
   *
   * @param bufID the vim buffer that is adressed
   * @param name the "name" of the command
   * @param param possible parameters
   * @see <a
   *      href="http://www.vim.org/htmldoc/netbeans.html#netbeans-protocol">Protocol
   *      specification</a>
   */
  public void command(int bufID, String name, String param) {
    int seqno = VimPlugin.getDefault().nextSeqNo();
    String cmd = bufID + ":" + name + "!" + seqno + " " + param;
    logger.debug("command: " + cmd);
    out.println(cmd);
  }

  /**
   * Sends a <i>function</i> (reply with a String as return value) as
   * specified by the netbeans-protocol to the vim instance.
   *
   * @param bufID the vim buffer that is adressed
   * @param name the "name" of the command
   * @param param possible parameters
   * @return the return value of the (vim)function or null if the function was
   * "saveAndExit".
   */
  public String function(int bufID, String name, String param)
    throws IOException
  {
    int seqno = VimPlugin.getDefault().nextSeqNo();
    String tmp = bufID + ":" + name + "/" + seqno + " " + param;
    logger.debug("function: " + tmp);

    // FIXME: need to find a better way to handle reading of function result
    // while run() is continously reading _all_ input from gvim.
    // FIXME: make use of the seqno for recognizing a function response.
    synchronized(functionMonitor){
      functionCalled = true;
      out.println(tmp);

      if ("saveAndExit".equals(name)){
        return null;
      }

      try{
        functionMonitor.wait(1000);
      }catch(InterruptedException ie){
        logger.error("Interrupted while waiting for function result.");
        return null;
      }
    }

    String result = functionResult;
    functionResult = null;
    logger.debug("result: " + result);
    return result;
  }

  /**
   * Sends a plain string to the vim instance. The user is responsible to
   * comply to the protocol syntax.
   *
   * @param s the string to send.
   * @see <a
   *      href="http://www.vim.org/htmldoc/netbeans.html#netbeans-protocol">Protocol
   *      specification</a>
   */
  public void plain(String s) {
    logger.debug("plain: " + s);
    out.println(s);
  }

  /**
   * Executes a --remote-send to the gvim instance.
   *
   * @param s The string to supply to the remote-send call.
   */
  public void remotesend(String s) {
    String[] args = {
      "vim", "--servername", String.valueOf(vimID), "--remote-send", s
    };
    try {
      logger.debug("remote-send: " + s);
      Process process = new ProcessBuilder(args).start();

      InputStream is = process.getInputStream();
      InputStreamReader isr = new InputStreamReader(is);
      BufferedReader br = new BufferedReader(isr);
      String line;
      while ((line = br.readLine()) != null) {
        logger.debug(line);
      }
      process.waitFor();

      logger.debug("result: " + process.exitValue());
    } catch (IOException ioe) {
      logger.error("Error sending command.", ioe);
    } catch (InterruptedException ie) {
      logger.error("Error sending command.", ie);
    }
  }

  /**
   * Adds a Listener to the list of observers. On each event all listeners are
   * informed about the event and may react to it. (Observer-Pattern).
   *
   * @param vl the new listener.
   */
  public void addListener(IVimListener vl) {
    listeners.add(vl);
  }

  /**
   * Simple Getter.
   *
   * @return the id of this connection
   */
  public int getVimID() {
    return vimID;
  }

  /**
   * Simple Setter.
   *
   * @param startupDone
   */
  public void setStartupDone(boolean startupDone) {
    this.startupDone = startupDone;
  }

  /**
   * Simple Getter.
   *
   * @return did Vim threw already "startupDone" Message?
   */
  public boolean isStartupDone() {
    return startupDone;
  }

  /**
   * Simple Setter.
   *
   * @param serverRunning
   */
  public void setServerRunning(boolean serverRunning) {
    this.serverRunning = serverRunning;
  }

  /**
   * Simple getter.
   *
   * @return whether a vim instance is running.
   */
  public boolean isServerRunning() {
    return serverRunning;
  }
}
